サンプルプログラム一式(cpp_test_program.zip)
はじめに
Windows上で走るMinGWのg++を使ってC++のお勉強をしましょう。
今回使用するg++(GCC)のバージョンは9.2.0です。
g++のバージョンを確認するには下記のコマンドを実行してみましょう。
g++ --version
コンパイルコマンドは下記のようになります。
今回はライブラリをスタティックリンクしようと思いますのでオプションをいくつか付けることにします。
g++ -o [出力ファイル名] [ソースファイル名] -static -lstdc++ -lgcc
なお、C++はC言語のスーパーセットということですのでC言語とかぶる部分については解説しませんので、 必要であればC言語を習得するの方をご覧ください。
サンプルプログラム一式(cpp_test_program.zip)はこちらからダウンロードしてください。
宣言と定義の違いについて
「宣言」と「定義」は異なり、宣言はコンパイラに知らせるためのもの、定義はメモリに確保される実体ということらしいです。 ただ、説明文を読んでいると宣言と定義が逆に使われていたり、同じ意味で使われていたりするので厳密には区別されていない気がします。 以下の説明でも本来の意味と逆になってしまっているところがあるかと思いますがご了承ください。
ソースプログラムを編集、コンパイルから実行するまでを簡単に行うために私(管理人) が制作しているテキストエディタ(TEditorMX)をご利用いただけると嬉しいです。
解説一覧
(1)参照
(2)右辺値参照
(3)関数のオーバーロード
(4)関数のデフォルト引数
(5)構造体
(6)ビットフィールド
(7)共用体
(8)無名共用体
(9)列挙型
(10)記憶クラス指定子
(11)アクセス修飾子
(12)newとdelete
(13)クラス
(14)コンストラクタとデストラクタ
(15)継承
(16)オブジェクトへのポインタ
(17)オブジェクトの参照
(18)クラスと構造体、共用体
(19)インライン化
(20)オブジェクトの引き渡し
(21)コンストラクタのオーバーロードし
(22)コピーコンストラクタ
(23)フレンド関数
(24)thisポインタ
(25)演算子のオーバーロードの基本
(26)2項演算子のオーバーロード
(27)2項演算子のオーバーロード(続き)
(28)関係演算子、論理演算子のオーバーロード
(29)単項演算子のオーバーロード
(30)フレンド演算子関数の使い方
(31)代入演算子のオーバーロード
(32)配列要素演算子のオーバーロード
(33)親クラスのアクセス制御
(34)protectedアクセス指定子
(35)コンストラクタとデストラクタと継承
(36)親クラスのコンストラクタへ引数を渡す方法
(37)多重継承
(38)仮想基本クラス
(39)子クラスへのポインタ
(40)仮想関数
(41)純粋仮想関数と抽象クラス
(42)汎用関数
(43)汎用クラス
(44)例外
(45)スタティックなクラスメンバ
(46)リンクとasm文
(47)変換関数
(1)参照
「参照」とはC言語で云うところのポインタ変数とほぼ同等なのですが、書き方が少し異なります。
それと「参照」を戻り値とする関数は代入式の左辺に記述することができます(これ重要です)。
プログラム中での書き方ですが、関数を定義するときにポインタでしたらアスタリスク(*)を付けますが、
「参照」の場合はアンパサンド(&)を付けます。あとは普通の変数のように使用できます。
文章よりサンプルプログラムをご覧いただいた方が早いと思いますので、下記のサンプルプログラムをご覧ください。
(ファイル名:1.ref.cpp)
#include <iostream> using namespace std; int func( int &a, int &b ) { cout << "a adr = " << &a << "\n"; cout << "b adr = " << &b << "\n"; a = 1; b = 3; return( a + b ); } int main() { int x, y, z; cout << "x adr = " << &x << "\n"; cout << "y adr = " << &y << "\n"; x = 10; y = 20; cout << "x, y = " << x << ", " << y << "\n"; z = func( x, y ); cout << "x, y, z = " << x << ", " << y << ", " << z << "\n"; }
上のサンプルプログラムを実行しますと下記のような結果になります(アドレス値そのものは実行した環境等に左右されます)。
まず、呼び出し側関数の変数x, yのアドレス値と呼び出された側の変数a, bのアドレス値が同じであることをご確認ください。
変数xと変数a、変数yと変数bのアドレスが同じなので、変数aに値を代入すると変数x値が、変数bに値を代入すると変数yの値がそれぞれ代わります。
x adr = 0x61ff08 y adr = 0x61ff04 x, y = 10, 20 a adr = 0x61ff08 b adr = 0x61ff04 x, y, z = 1, 3, 4
続いて関数についてです。
関数は「参照」を返すことができます。「参照」を返すということは代入式の左辺に記述することができます。
下記にサンプルプログラムを示します。
(ファイル名:1.ref_f.cpp)
#include <iostream> using namespace std; double val = 10.2; double &f( void ) { return( val ); } int main() { double v; v = f(); cout << "v = " << v << "\n"; f() = 201.5; cout << "val = " << val << "\n"; }
上のプログラムを実行しますと下記のような結果になります。
どうしてこうなるかお分かり頂けますか?。
まず最初の出力が v = 10.2 となっているのはグローバル変数の値が初期化時に10.2になっているからです。
これは普通にvalの値をリターンしたのと同じ結果ですね。
では次の出力 val = 201.5 はどうでしょうか?
サンプルプログラムでは関数fの呼び出しをしているのですが、それを代入式の左辺において201.5を代入しています。
これはエラーにはなりません。なぜなら関数fはグローバル変数valの「参照」を返してくるためグローバル変数valと実質同じになるためです。
そのためグローバル変数valの値は201.5になります。
v = 10.2 val = 201.5
(2)右辺値参照
通常、「参照」と言えば「左辺値参照」を言います。
「左辺値」とは変数に代入された値のことを言います。
それに対して「右辺値」とは変数に代入する前の演算結果や関数の戻り値、定数などを言います。
「右辺値」はコンパイラが内部的に用意するメモリに格納している値なので、不要になれば自動的に削除されてしまいます。
C言語のときは右辺値参照がなくても値の代入には大したロスはなかったのですが、
C++はオブジェクトが使えるため単純な代入式でも結構なロスになってしまうことがしばしばあると思います。
「右辺値参照」を用いますと余計な代入処理を省いて演算することが出来るようになりますので処理速度低下を防ぐことができます。
左辺値参照の変数にはアンパサンド(&)を用いますが、右辺値参照の変数にはアンパサンド2つ(&&)を用います。
下記にサンプルプログラムを示します。
(ファイル名:2.rightref.cpp)
#include <iostream> using namespace std; int f( int a ) { return( a * 2 ); } int main() { // 以下のx,y,zは右辺値参照の変数です。 int &&x = 1; // 1 は定数なので右辺値 int &&y = f(x); // 関数fの戻り値が右辺値 int &&z = x + 2; // x+2の演算結果が右辺値 cout << "x = " << x << "\r\n"; cout << "y = " << y << "\r\n"; cout << "z = " << z << "\r\n"; }
実行結果は下記のようになります。
x = 1 y = 2 z = 3
上記の例では単純な値に対する右辺値参照でしたが、オブジェクト(構造体も)に対しても同様です。
改めて変数に代入する必要が無い演算のときには、
右辺値参照を用いますとそれだけで速度低下を防ぐことができますので使ってみると良いでしょう。
そうそう、記述し忘れましたがコンパイラには「最適化」という処理がありまして、
単純な計算の場合には「最適化」処理により不要な代入による速度低下が起こらないことが多々あるかと思います。
よって、普通の(単純な)演算に右辺値参照を用いる必要はないかと思います。
(3)関数のオーバーロード
機能は同じだけど引数の数や型や違うという場合が結構よくあると思います。
そんな時、C言語の場合は微妙に異なる関数名を付けて対応していたと思います。
しかし、C++では関数のオーバーロードという機能のおかげで関数名が同じでも引数が異なれば異なる関数として認識してくれるようになりました。
下記のサンプルプログラムでは同じ関数名funcを3つ定義しています。
main関数では同じ関数名funcで呼び出していますが引数の数や型が異なるため、
それぞれ対応する関数を適切に呼び出すことができます。
(ファイル名:3.over.cpp)
#include <iostream> using namespace std; int func( int a ) { cout << "func( int a )\n"; return( a ); } int func( int a, int b ) { cout << "func( int a, int b )\n"; return( a ); } double func( double a, double b ) { cout << "func( double a, double b)\n"; return( a ); } int main() { int x, y, z; func( 1 ); func( 3, 4 ); func( 10.2, 30.4 ); }
上のサンプルプログラムを実行しますと、下記のような結果になります。
3つの関数は引数の数や型の違いにより適切に呼び出されているのが分かると思います。
func( int a ) func( int a, int b ) func( double a, double b)
(4)関数のデフォルト引数
「デフォルト引数」とは関数を呼び出す際、引数が省略された場合に利用されるデフォルト値のことです。
関数のデフォルト引数はプロトタイプ宣言のところで定義するか、もしくは関数定義のところでできます。
以下に関数のプロトタイプ宣言のところでデフォルト引数を定義する例(3.defarg.cpp)と、
関数定義のところでデフォルト引数を定義する例(3.defarg_2.cpp)を示します。
どちらもやっていることは同じです。
この場合、引数bまたは引数a、bの両方を省略してデフォルト引数の値を利用することができます。
引数aを省略して引数bだけを渡すということはできません。
繰り返しになりますが、サンプルプログラムのコメントにも記述してある通り、
左側の方の引数を省略することはできません。
頻繁に省略する可能性のある引数は右側の方へ記述するようにしましょう。
(ファイル名:4.defarg.cpp)
#include <iostream> using namespace std; void func( int a = 1, int b = 2 ); int main() { func(); func(50); func( 99, 150 ); // func( , 200); // この行を有効にするとエラーになります。 } void func( int a, int b ) { cout << "a, b = " << a << ", " << b << "\n"; }
(ファイル名:4.defarg_2.cpp)
#include <iostream> using namespace std; void func( int a = 5, int b = 10 ) { cout << "a, b = " << a << ", " << b << "\n"; } int main() { func(); func(50); func( 99, 150 ); // func( , 200); // この行を有効にするとエラーになります。 }
上のプログラムの実行結果は下記のとおりです(両方とも同じ結果になります)。
a, b = 1, 2 a, b = 50, 2 a, b = 99, 150
(5)構造体
C++の構造体はC言語の構造体と記述の仕方では互換性があります。
ただC++の場合、構造体は型として扱われるそうなのでC言語より扱いが簡単です。
どのように簡単になったのか、サンプルプログラムで違いをみてみましょう。
(ファイル名:5.struct.cpp)
#include <iostream> using namespace std; struct TAG { int a; double b; }; void func( TAG &s ) { s.a = 10; s.b = 10.1; cout << "&s = " << &s << "\n"; } int main() { TAG s; cout << "call &s = " << &s << "\n"; func( s ); cout << "s.a = " << s.a << "\ns.b = " << s.b << "\n"; }
上のプログラムでは、まず構造体TAGを定義しています。
C言語ではTAGは「型名」ではなく「タグ名」なので変数を宣言する際にはstructを付ける必要がありました。
しかし、C++では「型名」として扱われるためmain関数のところでstructを付けなくても変数sを宣言できています。
またfunc関数の引数のところも同様です。
それともうひとつ、このプログラムでは構造体を「参照」しています。
func関数の引数のところでアンパサンド(&)を付けています。
coutの表示のところでアドレスを表示していますが、どちらも同じ値になりますので参照していることが確認できます。
下記に上記のプログラムの実行結果の例を示しますので、構造体sのアドレス値などの結果をご覧ください。
call &s = 0x61ff00 &s = 0x61ff00 s.a = 10 s.b = 10.1
(6)ビットフィールド
ビットフィールドとは、ビット単位で指定する構造体のことです。
ビット単位で指定できますので、
仮に本来ならchar型4つで格納するデータをchar型1つにまとめるということも可能になってきます。
もう少し具体的に言いますと、例えばフラグが4つあったとします。
フラグなので1つ当たり1ビットあれば充分です。
これをビットフィールドを使って1バイトに収めてしまう...ということです。
(ファイル名:6.bit.cpp)
#include <iostream> using namespace std; struct BITTAG { char fOne: 1; // フラグ1 char fTwo: 1; // フラグ2 char fTree: 1; // フラグ3 char fFour: 1; // フラグ4 }; int main() { BITTAG b; b.fOne = 1; b.fTwo = 0; b.fTree = 0; b.fFour = 1; if( b.fOne ) cout << "b.fOne is true\n"; if( b.fTwo ) cout << "b.fTwo is true\n"; if( b.fTree ) cout << "b.fTree is true\n"; if( b.fFour ) cout << "b.fFour is true\n"; cout << "b size = " << sizeof(b) << "\n"; }
上記プログラムを実行すると下記のようになります。
設定した通りに出力になっていて、
構造体bのサイズも1バイト(構造体定義のところでchar型宣言しています)に収まっています。
b.fOne is true b.fFour is true b size = 1
(7)共用体
共用体も構造体同様に「タグ名」であったものは「型名」になりました。
そのため変数を宣言するときにunionを付ける必要はありません。
下記にサンプルプログラムを示します。
(ファイル名:7.union.cpp)
#include <iostream> using namespace std; struct INTCHAR { char c0; char c1; char c2; char c3; }; union INT { INTCHAR c; int i; }; int main() { INT a; a.i = 0x12345678; cout.setf(std::ios::hex, std::ios::basefield); // 16進数で出力指定 cout << "i = 0x" << a.i << "\n"; cout << "c0 = 0x" << (int)a.c.c0 << "\n"; cout << "c1 = 0x" << (int)a.c.c1 << "\n"; cout << "c2 = 0x" << (int)a.c.c2 << "\n"; cout << "c3 = 0x" << (int)a.c.c3 << "\n"; }
注意して頂きたいところはmain関数の共用体変数aを宣言しているところでunionを使っていないというところです。
C++では共用体を定義すると「タグ名」ではなく「型名」になります。
これは構造体と同様です。
ちなみに上記プログラムを実行した結果は下記のようになります。
i = 0x12345678 c0 = 0x78 c1 = 0x56 c2 = 0x34 c3 = 0x12
(8)無名共用体
C++には「無名共用体」というものがあります。
無名共用体とは型名を持たない(付けない)共用体です。
無名共用体は型名を持たないので変数も宣言しません。
直接メンバを変数のように扱います。
下記にサンプルプログラムを示しますのでご覧ください。
(ファイル名:8.noname.cpp)
#include <iostream> using namespace std; int main() { union { char c[4]; int i; }; int j; i = 0x12345678; // 無名共用体のメンバiに値を代入 cout.setf(std::ios::hex, std::ios::basefield); // 16進数で出力指定 cout << "i = 0x" << i << "\n"; for( j = 0; j < 4; j++ ) { cout << "c[" << j << "] = " << (int)c[j] << "\n"; } }
実行結果は下記のようになります。
ちょっとしたことに共用体を使いたいときには便利かもしれませんね。
i = 0x12345678 c[0] = 78 c[1] = 56 c[2] = 34 c[3] = 12
(9)列挙型
C++では列挙型も構造体、共用体同様、「タグ名」は「型名」になりました。
その他の使い方はC言語と同様です。
下記にサンプルプログラムを示します
(ファイル名:9.enum.cpp)
#include <iostream> using namespace std; enum COLOR { RED, GREEN, BLUE, COLOR_NUM }; int main() { COLOR clr; int i; for( i = 0; i < COLOR_NUM; i++ ) { if( i == (int)RED ) cout << "RED = " << i << "\n"; if( i == (int)GREEN ) cout << "GREEN = " << i << "\n"; if( i == (int)BLUE ) cout << "BLUE = " << i << "\n"; } }
上記プログラムはサンプルプログラムとしてどうかと思うところもありますが、
一応どんな感じかはご理解頂けるのではないかと思います。
「タグ名」が「型名」になったので「enumを付けなくても宣言できます」ということがご理解頂ければ良いかと思います。
ちなみに実行結果は下記のようになります。
RED = 0 GREEN = 1 BLUE = 2
(10)記憶クラス指定子
C++には変数の格納方法を指定する、下記のような記憶クラス指定子というものがあります(C言語にもありました)。
- auto
- register
- extern
- static
C言語にもありましたのでautoとextern以外の説明は割愛します。
まずはautoですが、C++11から変数の型を初期化子から推論できるようになりました。
これにより初期化子を指定するときは、複雑な型を記述しなくてもautoとすれば簡単に宣言できるようになりました。
auto i = 0; // i は int 型 const auto l = 0L; // l は const long 型 auto &r = i; // r は int& 型 auto s = ""; // s は const char* 型
またexternですが、ちょっと変わった使い方がありますので紹介します。
(ファイル名:10.memclass.cpp)
#include <iostream> using namespace std; int i; void func( void ) { extern int i; i++; } int main() { extern int i; i = 10; cout << "berore: i = " << i << "\n"; func(); cout << "after: i = " << i << "\n"; }
上記のように関数内部でexternを用いますとグローバル変数を参照します。
おそらくこういう使い方はしないと思いますが、こんなこともできますということです。
ちなみに実行結果は下記のようになります。
berore: i = 10 after: i = 11
(11)アクセス修飾子
constとvolatileの2つのアクセス修飾子があります。
まずconstですが、変数の前にconstを付けるとその変数は定数のように扱われ、プログラムから変更できなくなります。
ただし、初期化はできます。
(ファイル名:11.const.cpp)
#include <iostream> using namespace std; const int i = 10; // 10に初期化はできます。 int main() { cout << "i = " << i << "\n"; i = 50; // これはコンパイルエラーになります。 }
次にvolatileですが、これは割り込み処理などで不用意に書き代わってしまう変数を扱うとき、
常にメモリから値を読み込んでから使う(つまり最適化させない)ための修飾子です。
const同様、宣言するときに変数名の前に付けます。
volatile int interrupt; // こんな感じで宣言します。
(12)newとdelete
new演算子を使うと動的にメモリを確保することができます。
new演算子はメモリ確保に失敗したときはNULLを返します。
[ポインタ変数] = new [変数の型];
new演算子で動的にメモリを確保するとき「変数の型」の後に括弧を付けて初期化することができます
ポインタ変数 new 変数の型 ( 初期値 );
delete演算子を使うとnew演算子で確保したメモリを解放することができます。
delete ポインタ変数;
下記にサンプルプログラムを示します。
(ファイル名:12.newdel.cpp)
#include <iostream> using namespace std; int main() { int *pi, *pj; pi = new int; if( ! pi ) { cout << "メモリ確保エラー\n"; return( 1 ); } *pi = 10; cout << "*pi = " << *pi << "\n"; pj = new int(120); cout << "*pj = " << *pj << "\n"; return( 0 ); }
new演算子では配列のメモリも確保することができます。
ただし配列のメモリを確保する場合は初期化できません。
ポインタ変数 = new 変数の型 [サイズ];
delete演算子で確保した配列のメモリを解放することができます。
delete [size] ポインタ変数;
※古いC++コンパイラではsizeを指定する必要がありましたが、
新しいC++コンパイラでは不要です。
下記にサンプルプログラムを示します。
(ファイル名:12.newdel2.cpp)
#include <iostream> using namespace std; int main() { int i, *pArr; pArr = new int [10]; if( ! pArr ) { cout << "メモリ確保エラー\n"; return( 1 ); } for( i = 0; i < 10; i++ ) { pArr[i] = i + 1; } for( i = 0; i < 10; i++ ) { cout << pArr[i] << "\n"; } delete [] pArr; }
(13)クラス
classの定義の構文は構造体の定義に似ています。
class クラス名 { // private属性の関数と変数 public: // public属性の関数と変数 } オブジェクトリスト;
- 「オブジェクトリスト」はオプション(してしなくてもOK)です。
- class宣言の中で宣言される関数と変数はそのクラスのメンバと呼ばれています。
- デフォルトで宣言(定義)されている関数や変数はすべてprivate属性です。
- privateで宣言されたメンバ変数はメンバ関数からしかアクセスできません。
以下にクラス定義と使い方の簡単なサンプルプログラムを示します。
(ファイル名:13.class1.cpp)
#include <iostream> using namespace std; class myclass { int a; public: void set_a( int num ) { a = num; } int get_a( void ) { return a; } }; int main() { myclass obj1, obj2; obj1.set_a( 99 ); obj2.set_a( 200 ); cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; }
少し解説します。
private宣言されているメンバ変数aはクラス内のメンバ関数からしかアクセスできません。
ここではメンバ関数set_aとメンバ関数get_aを用いてメンバ変数aの値を操作しています。
main関数の先頭でmyclassのオブジェクトobj1とobj2を宣言(定義)しています。
次にset_aを用いてそれぞれのオブジェクトのメンバ変数aに値(99と200)を設定しています。
get_aを用いてobj1、obj2それぞれのメンバ変数aの値を表示しています。
結果は下記のようになります。
99 200
上のサンプルプログラムではメンバ関数をクラス内に定義しましたが、
メンバ関数が大きくなってくると窮屈になってきます。
そんなときは、解決演算子 :: を用いてクラス外に定義することができます。
以下にその例を示します。
(ファイル名:13.class2.cpp)
#include <iostream> using namespace std; class myclass { int a; public: void set_a( int num ); int get_a( void ); }; void myclass::set_a( int num ) { a = num; } int myclass::get_a( void ) { return a; } int main() { myclass obj1, obj2; obj1.set_a( 99 ); obj2.set_a( 200 ); cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; }
myclass内ではメンバ関数の宣言をしているだけで、その実態はクラスの外で定義しています。
もうひとつ、メンバ変数aをpublic宣言したときにmain関数から直接アクセスできることを確認してみましょう。
(ファイル名:13.class3.cpp)
#include <iostream> using namespace std; class myclass { public: int a; }; int main() { myclass obj1, obj2; obj1.a = 99; obj2.a = 200; cout << obj1.a << "\n"; cout << obj2.a << "\n"; }
(14)コンストラクタとデストラクタ
「コンストラクタ」とはオブジェクトが作成されるときに自動的に呼び出される関数です。
コンストラクタはクラス名と同じ名前になります。また、戻り値はありません。
コンストラクタはオブジェクトが作成されるときに必要なメモリを確保したり、変数に初期値を与えたりするのに使われます。
「デストラクタ」とはオブジェクトが破棄されるときに自動的に呼び出される関数です。
デストラクタはクラス名の前にチルダ ~ を付けた名前になります。また引数、戻り値はありません。
デストラクタはオブジェクトで作成したメモリを解放したりするのに使われます。
コンストラクタとデストラクタはクラス外部の関数から呼び出されることになるのでpublic属性でなければなりません。
なお、コンストラクタとデストラクタには1つ制限があります。それは「アドレスを取得できない」ということです。
ではサンプルプログラムを示します。
(ファイル名:14.condes1.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( void ); // コンストラクタ ~myclass( void ); // デストラクタ void show( void ); }; myclass::myclass( void ) { cout << "In コンストラクタ\n"; a = 100; } myclass::~myclass( void ) { cout << "In デストラクタ\n"; } void myclass::show( void ) { cout << a << "\n"; } int main() { myclass obj; // ここでコンストラクタが呼び出されます。 obj.show(); return( 0 ); // ここでデストラクタが呼び出されます。 }
結果は下記のようになります。
In コンストラクタ 100 In デストラクタ
あらたまってコンストラクタやデストラクタを呼び出さなくてもオブジェクトが作成されるとき、
または破棄されるときに自動的に呼び出されることが確認できました。
続いて、コンストラクタには引数を渡すことができます。
引数は宣言時に括弧を付けて関数を呼び出すような感じで渡します。
下記にサンプルプログラムを示します。
(ファイル名:14.condes2.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int ); // コンストラクタ ~myclass( void ); // デストラクタ void show( void ); }; myclass::myclass( int num ) { cout << "In コンストラクタ\n"; a = num; } myclass::~myclass( void ) { cout << "In デストラクタ\n"; } void myclass::show( void ) { cout << a << "\n"; } int main() { myclass obj( 5 ); // myclass obj = myclass( 5 ); // myclass obj = 5; obj.show(); return( 0 ); }
結果は下記のようになります。
In コンストラクタ 5 In デストラクタ
結果は初期値として与えている 5 が表示されました。
コンストラクタに引数を渡す方法(書き方)はあと2種類あります。
宣言時の書き方が少し異なるだけなので、その部分だけ下記に示します。
myclass obj = myclass( 5 ); myclass obj = 5;
下の方の渡し方は引数がひとつだけのときしか使えません。
最後に、デストラクタには引数を渡すことはできません。また、引数を渡す必要もありません。
(15)継承
継承元となるクラスを、
基底クラス、親クラス、スーパークラス
などと呼びます。
継承先となるクラスを、
派生クラス、子クラス、サブクラス
などと呼びます。
親クラスは子クラスに共通するすべての特性を定義します。
子クラスは一般的な特性を継承し、それぞれのクラスに固有の特性を追加したものになります。
下記に継承のサンプルプログラムを示します。
(ファイル名:15.inher.cpp)
#include <iostream> using namespace std; class Parent { int a; public: void set_a( int num ) { a = num; } int get_a( void ) { return a; } }; class Child: public Parent { int b; public: void set_b( int num ) { b = num; } int add( void ) { return( get_a() + b ); } }; int main() { Child obj; obj.set_a( 10 ); obj.set_b( 20 ); cout << obj.add() << "\n"; return( 0 ); }
親クラスのpublicなメンバはそのまま子クラスのメンバになります。
子クラスのメンバ関数は親クラスのメンバ変数aには直接アクセスできません(private属性なので)。
そのためget_aメンバ関数を呼び出していることに注意してください。
アクセス指定子は、
public, private, protected
がありますが、とりあえずクラスの継承時はpublicを使うことにします。
上記プログラムの実行結果は下記のようになります。
30
(16)オブジェクトへのポインタ
オブジェクトへのポインタは構造体や共用体と同じような感じで扱うことができます
オブジェクト変数の前にアンパサンド(&)を付けるとオブジェクトのアドレスを取得できます。
アロー演算子(->)を使ってメンバにアクセスできます。
下記にサンプルプログラムを示します。
(ファイル名:16.objptr.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int num ) { a = num; } int get_a( void ) { return a; } }; int main() { myclass obj( 10 ); myclass *p; p = &obj; cout << p->get_a() << "\n"; return( 0 ); }
オブジェクトへのポインタはオブジェクトを作成しているわけではありませんので注意してください。
上記プログラムの実行結果は下記のようになります。
10
(17)オブジェクトの参照
オブジェクトは他のデータ型と同様に「参照」することができます。
下記にサンプルプログラムを示します。
(ファイル名:17.objref.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int num ) { a = num; } void set_a( int num ) { a = num; } int get_a( void ) { return a; } }; void func( myclass &o ) { o.set_a( 15 ); } int main() { myclass obj( 10 ); cout << obj.get_a() << "\n"; func( obj ); cout << obj.get_a() << "\n"; return( 0 ); }
結果は下記のようになります。
10 15
オブジェクトも普通に「参照」することができました。
(18)クラスと構造体、共用体
C++ではクラスと構造体は本質的に同じものです。
クラスと構造体が異なるのはデフォルトがpublicかprivateかというだけです。
構造体もコンストラクタ・デストラクタを持つことができます。
下記に構造体を使ったクラスのサンプルプログラムを示します。
(ファイル名:18.clstruct1.cpp)
#include <iostream> using namespace std; struct mystruct { private: int a; public: mystruct( int num ) { a = num; } void set_a( int num ) { a = num; } int get_a( void ) { return a; } }; int main() { mystruct obj( 20 ); cout << obj.get_a() << "\n"; obj.set_a( 9 ); cout << obj.get_a() << "\n"; }
実行結果は下記のようになります。
20 9
共用体も構造体同様、クラスと関係があります。
共用体もメンバ関数を持つことができます。
メンバ変数だけメモリを共用します。
ここがクラスや構造体と異なるところです。
下記に共用体を使ったクラスのサンプルプログラムを示します。
(ファイル名:18.clstruct2.cpp)
#include <iostream> using namespace std; struct INTCHAR { char c0; char c1; char c2; char c3; }; union myunion { private: INTCHAR c; int a; public: myunion( int num ) { a = num; } void print( void ); }; void myunion::print( void ) { cout.setf(std::ios::hex, std::ios::basefield); // 16進数で出力指定 cout << "a = 0x" << a << "\n"; cout << "c0 = 0x" << (int)c.c0 << "\n"; cout << "c1 = 0x" << (int)c.c1 << "\n"; cout << "c2 = 0x" << (int)c.c2 << "\n"; cout << "c3 = 0x" << (int)c.c3 << "\n"; } int main() { myunion obj( 0x12345678 ); obj.print(); }
実行結果は下記のようになります。
a = 0x12345678 c0 = 0x78 c1 = 0x56 c2 = 0x34 c3 = 0x12
以上のように、構造体も共用体もクラスのように使うことが可能です。
使い方はプログラマ次第といったところでしょうか?
(19)インライン化
関数を呼び出すと「関数へ制御を移してローカル変数領域を確保」してからプログラムを実行することになります。
また実行を終了するときは「ローカル変数領域を解放してから制御を元に戻す」という処理が入ります。
インライン化すると「制御を移してローカル変数領域を確保」
「ローカル変数領域を解放してから制御を元に戻す」という処理がなくなる代わりに、
処理がすべてメモリ上に展開されるためプログラムが大きくなる傾向にあります。
インライン化すべき関数(処理)とは、非常に少ない処理のときだけでしょう。
あまり大きな処理をインライン化してしまうとCPUのキャッシュメモリを圧迫して返ってシステム全体が遅くなってしまうかもしれません。
C++ではインライン化する方法は2種類あります。
ひとつはクラス内に定義してしまう方法。
もうひとつは関数定義時にinline指定子を付けることです。
下記にサンプルプログラムを示します。
(ファイル名:19.inline.cpp)
#include <iostream> using namespace std; class myclass { int a; public: // set_aはインライン化されます。 void set_a( int num ) { a = num; } int get_a(); }; // get_aはインライン化されます inline int myclass::get_a( void ) { return a; } int main() { myclass obj1, obj2; obj1.set_a( 99 ); obj2.set_a( 200 ); cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; }
プログラムでインライン化を指示してもコンパイラが受け付けてくれない(無視される)こともあるようです
(再起呼び出しされる関数など)。
(20)オブジェクトの引き渡し
オブジェクト変数を = で代入したらどうなるでしょうか?
この場合、バイナリレベルで単にコピーされるだけです。
下記にサンプルプログラムを示します。
(ファイル名:20.argret1.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int num = 0 ) { cout << "In コンストラクタ\n"; a = num; } ~myclass() { cout << "In デストラクタ\n"; } void set_a( int num ) { a = num; } int get_a() { return a; } }; int main() { myclass obj1( 20 ); myclass obj2; cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; obj2 = obj1; cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; }
結果は下記のようになります。
In コンストラクタ In コンストラクタ 20 0 20 20 In デストラクタ In デストラクタ
obj1がobj2にコピーされています。
ここまでは想像するに難しくないと思います。
ではオブジェクトを関数に渡したらどうなるでしょうか?
「コピーしたときと同じじゃないの?」と思われた方、不正解です。
オブジェクトを関数に渡すとコンストラクタが呼び出されません。
理由は「初期化する必要が無いから」です。
下記にサンプルプログラムを示します。
(ファイル名:20.argret2.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int num = 0 ) { cout << "In コンストラクタ\n"; a = num; } ~myclass() { cout << "In デストラクタ\n"; } void set_a( int num ) { a = num; } int get_a() { return a; } }; int add( myclass o ) { return o.get_a() + o.get_a(); } int main() { myclass obj( 19 ); cout << add( obj ) << "\n"; }
結果は下記のようになります。
In コンストラクタ 38 In デストラクタ In デストラクタ
上記の結果から分かるようにコンストラクタは初期化時の1回しか呼び出されていません。
それに対しデストラクタは2回呼び出されています。
これはadd関数終了時に1回呼び出され、main関数終了時にもう1回呼び出されています。
では最後に戻り値としてオブジェクトを返す場合にはどうなるでしょうか?
オブジェクトを返すとき、一時的にですが自動的の新しいオブジェクトが作成され、
引数を渡したら削除されます。そのため下記のサンプルプログラムを実行すると3回デストラクタが呼び出されます。
(ファイル名:20.argret3.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( int num = 0 ) { cout << "In コンストラクタ\n"; a = num; } ~myclass() { cout << "In デストラクタ\n"; } void set_a( int num ) { a = num; } int get_a() { return a; } }; myclass func( void ) { myclass obj; obj.set_a(25); // MinGwで下のreturn文を実行すると動作が何故か異なります! // return obj; return( obj ); } int main() { myclass obj; obj = func(); cout << obj.get_a() << "\n"; }
実行結果は下記のようになります。
In コンストラクタ In コンストラクタ In デストラクタ In デストラクタ 25 In デストラクタ
上記の結果ようにデストラクタが3回呼び出されます。
1回目はfunc関数が終了するとき、2回目は自動的に作成されたオブジェクトが破棄されるとき、
3回目はmain関数が終了するとき、です。
ちなみに...なんですが、プログラム中のコメントにも記述しておいたのですが、
MinGWのg++だとfunc関数のreturn文のところを括弧で括った時と括らなかったときで何故か動作が違いました。
デストラクタが2回しか呼ばれない...g++が最適化をしてしまっているのでしょうか?
謎です。
更に「ちなみに」なのですが、気になったのでVisualStudio2017のC++コンパイラでも実行して確認してみたのですが、
デストラクタは3回呼び出されていました(上記の結果と同じでした)。
(21)コンストラクタのオーバーロード
コンストラクタはオーバーロードできます。
ちなみに、デストラクタはオーバーロードできません。
下記にサンプルプログラムを示します。
(ファイル名:21.constover.cpp)
#include <iostream> using namespace std; class myclass { int a; public: myclass( void ) { a = 8; } myclass( int num ) { a = num; } int get_a() { return a; } }; int main() { myclass obj1(99); myclass obj2; cout << obj1.get_a() << "\n"; cout << obj2.get_a() << "\n"; return( 0 ); }
結果は下記のようになります。
99 8
オーバーロードできているのが確認できました。
(22)コピーコンストラクタ
コピーコンストラクタを使うと初期化時の動的メモリ割り当てされた変数の問題に対応できます
(コピーコンストラクタが呼び出されるのは初期化時だけです)。
特に戻り値としてオブジェクトを返したい場合には有効と思われます。 引数で渡すときは参照やポインタで渡した方が処理速度的にも有効ではないかと思われます。
コピーコンストラクタの書式
クラス名 (const クラス名 &obj) { // objはオブジェクトの変数名
コンストラクタ本体(動的メモリ割り当て)
}
コピーコンストラクタに他の引数を持たせることもできますが、デフォルト引数の定義が必要です。
第1引数は初期化を行うオブジェクトの参照でなければなりません。
constを指定しなくても可能ですが、別のオブジェクトの初期化にconstオブジェクトが使えなくなります。
下記にコピーコンストラクタを使ったサンプルプログラムを示します。
(ファイル名:22.copyconst.cpp)
#include <iostream> #include <string.h> using namespace std; class myclass { char *s; int size; public: myclass( void ) { cout << "In コンストラクタ\n"; s = NULL; size = 0; } myclass( const myclass & ); ~myclass() { cout << "In デストラクタ\n"; delete [] s; } void print( void ); void set( char *str ); }; myclass::myclass( const myclass &obj ) { cout << "In コピーコンストラクタ\n"; s = new char [obj.size+1]; if ( ! s ) { cout << "メモリ確保エラー\n"; exit(1); } strcpy( s, obj.s ); } void myclass::print( void ) { cout << s << "\n"; } void myclass::set( char *str ) { size = strlen( str ); s = new char[size+1]; strcpy( s, str ); } myclass func( void ) { myclass obj; cout << "In func関数\n"; obj.set( (char *)"Hello World!!" ); return( obj ); } int main() { myclass obj; obj = func(); obj.print(); return( 0 ); }
結果は下記のようになります。
In コンストラクタ In コンストラクタ In func関数 In コピーコンストラクタ In デストラクタ In デストラクタ Hello World!! In デストラクタ
コピーコンストラクタが呼び出されているのは自動的に作成されるオブジェクトが作成されたときの1回だけです。
(23)フレンド関数
クラス内でフレンド宣言された関数(メンバ関数ではなく、普通の関数)は、クラスのprivate属性のメンバにアクセスできます。
下記にサンプルプログラムを示します。
(ファイル名:23.friend1.cpp)
#include <iostream> using namespace std; class myclass { int a, b; public: myclass( int i, int j ) { a = i; b = j; } friend int add( myclass ); }; // ここでフレンド関数を定義します。 // aとbを加算した結果を返します。 int add( myclass obj ) { return( obj.a + obj.b ); } int main() { myclass obj(10,12); cout << add( obj ) << "\n"; return( 0 ); }
結果は下記のようになります。
22
フレンド関数はオブジェクトを通じてしか、それら(オブジェクトのメンバ)にアクセスすることはできません。
一般的にフレンド関数にはオブジェクトを渡します。
フレンド関数は子クラスには継承されません。
親クラスではフレンド関数でも子クラスではフレンド関数ではなくなります。
フレンド関数は異なるクラスの共通して持つ数量を比較するような場合に使います。
下記にサンプルプログラムを示します。
(ファイル名:23.friend2.cpp)
#include <iostream> using namespace std; class truck; // 前方宣言(bikeのフレンド関数を宣言するときに使うため) class bike { int speed; public: bike( int sp ) { speed = sp; } friend int speed_cmp( bike, truck ); }; class truck { int speed; public: truck( int sp ) { speed = sp; } friend int speed_cmp( bike, truck ); }; // bike > truck のとき正数 // bike == truckのとき0 // bike < truck のとき負数 int speed_cmp( bike b, truck t ) { return( b.speed - t.speed ); } int main() { bike b1(100), b2(60), b3(40); truck t(60); cout << speed_cmp(b1,t) << "\n"; cout << speed_cmp(b2,t) << "\n"; cout << speed_cmp(b3,t) << "\n"; return( 0 ); }
結果は下記のようになります。
40 0 -20
bikeクラスのfriend宣言のところでまだ宣言されていないtruckを使用するため、
前もってtruckクラスがあることをbikeの定義前に宣言していることに注意してください。
(24)thisポインタ
まずは下記のプログラムを見てください。
(ファイル名:24.this1.cpp)
#include <iostream> using namespace std; class myclass { int a, b; public: myclass( int i, int j ) { a = i; b = j; } int get_a( void ) { return a; } int get_b( void ) { return b; } }; int main( void ) { myclass obj(15, 20); cout << obj.get_a() << "\n"; cout << obj.get_b() << "\n"; }
メンバ変数a、bをコンストラクタで初期化して、get_a、get_bメソッドで値を返すだけのプログラムです。
thisポインタはすべてのメンバ関数へ自動的に引き渡されるポインタです。
thisポインタは呼び出したオブジェクトへのポインタが入っています。
(上のプログラムではmain関数のobj変数へのポインタ)
よって、上のプログラム中で使われているメンバ変数a、bは、
this->a、this->b
と書き換えることができます。
thisポインタを使った書き方の方が本来の書き方といえます。
単にa、bと表記したものはthisポインタを使った略式表記といえます。
上のプログラムのmyclassクラスをthisポインタを使って書き直すと下記のようになります。
(ファイル名:24.this2.cpp)
#include <iostream> using namespace std; class myclass { int a, b; public: myclass( int i, int j ) { this->a = i; this->b = j; } int get_a( void ) { return this->a; } int get_b( void ) { return this->b; } }; int main( void ) { myclass obj(15, 20); cout << obj.get_a() << "\n"; cout << obj.get_b() << "\n"; }
このままですとthisポインタには役目は無いような感じですが、
演算子のオーバーロードのときなど、いくつか使い道があります。
次以降で解説します。
(25)演算子のオーバーロードの基本
まずはメンバ演算子関数について解説します。
戻り値のクラス クラス名::operator# (引数リスト)
「戻り値のクラス」は通常、定義されているクラスになりますが、別に他のクラスでも構いません。
オーバーロードする演算子が + の場合、
関数名は、 operator+ になります(#にはオーバーロードする演算子が入ります。)。
「引数リスト」は演算子関数の実装方法とオーバーロードする演算子の型に応じて変わってきます。
演算子のオーバーロードの制限が2つあります。
- 演算子の優先順位は変更できません。
- 演算子のとるオペランドの数は変更できません。
演算子がオーバーロードされても元の意味が失われるのではなく、
定義されたクラスに関連付けられた意味が追加されるだけです。
オーバーロードできない演算子(4つ)
. :: .* ?
代入演算子を除き、演算子関数は子クラスに継承されます。
(26)2項演算子のオーバーロード
まずはサンプルプログラムを示します。
(ファイル名:26.optwo.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } point operator+( point ); }; point point::operator+( point o2 ) { point temp; temp.x = x + o2.x; temp.y = y + o2.y; return( temp ); } int main( void ) { point o1(3,5); point o2(10); point o3(6,2); point o4; int x, y; o4 = o1 + o2; o4.get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; (o1+o2+o3).get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; }
演算子 + をオーバーロードして、pointクラスのx、yメンバをそれぞれ加算できるようにしたものです。
実行結果は下記のようになります。
x = 13, y = 5 x = 19, y = 7
上の行はo1とo2のx、yメンバをそれぞれ加算した値になっています。
下の行は...ソースプログラムの方でちょっと変わった書き方をしているのですが問題なくo1~o3の値を加算して表示しています。
注意点としては演算子の左側のオペランド(オブジェクト)を関数へ渡し(this)、
右側のオペランドを引数へ渡します。
(27)2項演算子のオーバーロード(続き)
(25)のプログラムを拡張します。
- -演算子で減算できるようにします。
- =演算子で代入できるようにします。
ではサンプルプログラムを示します。
(ファイル名:27.optwocont.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } point operator+( point ); point operator-( point ); point operator=( point ); point operator=( int ); }; point point::operator+( point o2 ) { point temp; temp.x = x + o2.x; temp.y = y + o2.y; return( temp ); } point point::operator-( point o2 ) { point temp; temp.x = x - o2.x; temp.y = y - o2.y; return( temp ); } point point::operator=( point o2 ) { point temp; x = o2.x; y = o2.y; return( *this ); // 代入されたオブジェクトを返す! } point point::operator=( int num ) { point temp; x = num; y = num; return( *this ); // 代入されたオブジェクトを返す! } int main( void ) { point o1(3,5); point o2(10); point o3(6,2); point o4; int x, y; o4 = o1 + o2; o4.get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; o4 = o2 - o1; o4.get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; (o1+o2-o3).get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; }
-演算子と=演算子を追加しました。
特に=演算子はint型の引数を受け取るようにしたことで整数値で初期化できるようになりました。
これでメンバを0クリアするのも簡単になりました。
サンプルプログラムでは引数で普通にオブジェクト(のコピー)を受け取っていますが、
処理速度が低下することも考えられますので、特に支障が無ければ「参照」を使った方が良いかもしれません。
実行結果は下記のようになります。
x = 13, y = 5 x = 7, y = -5 x = 7, y = 3 x = 0, y = 0
(28)関係演算子、論理演算子のオーバーロード
関係演算子や論理演算子もオーバーロードすることができます。
関係演算子や論理演算子が返す値はtrueかfalseなので、それに対応する値(通常1か0)を返すようにすればOKです。
下記にサンプルプログラムを示します。
(ファイル名:28.opronri.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } int operator&&( point & ); int operator==( point & ); }; int point::operator&&( point &o2 ) { int ret; ret = ((x && o2.x)? 1: 0); ret |= ((y && o2.y)? 2: 0); return( ret ); } int point::operator==( point &o2 ) { int ret; ret = (((x == o2.x) && (y == o2.y))? 1: 0); return( ret ); } int main( void ) { point o1(3,5); point o2(3,5); point o3(1,0); point o4(0,1); if( o1 == o2 ) cout << "o1 == o2\n"; else cout << "o1 != o2\n"; cout << "o1 && o3 = " << (o1 && o3) << "\n"; cout << "o1 && o4 = " << (o1 && o4) << "\n"; }
実行結果は下記のようになります。
o1 == o2 o1 && o3 = 1 o1 && o4 = 2
1行目のo1とo2は同じ値で初期化していますのでOKです。
2行目はxがtrueでyがfalseになりますので1でOKです。
3行目はxがfalseでyがtrueになりますので2でOKです。
以上、期待した結果が返ってきました。
やっていることは(25)の2項演算子と同じです。
少し異なるのは引数で受け取っているオブジェクトを「参照」にしてみたくらいです。
(29)単項演算子のオーバーロード
単項演算子のオーバーロードについてです。
単項演算子は1つしかオペランドを持ちません。
オペランドが1つなのでそのオペランドが演算子関数の呼び出しを生成します。
通常、単項演算子は右側にオペランドが付くのですが、
ポストインクリメント演算子とポストデクリメント演算子はその左側に付きます。
以前のC++ではこれら(プリとポスト)は区別できなかったそうなのですが、
新しいC++コンパイラはダミー引数を受け取ることで区別できるようになっています。
ではサンプルプログラムを示します。
(ファイル名:29.opone.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } point operator++(); // プリインクリメント point operator++( int x ); // ポストインクリメント }; point point::operator++() // プリインクリメント { cout << "In プリインクリメント\n"; ++x; ++y; return( *this ); } point point::operator++( int n ) // ポストインクリメント(nはダミーです) { cout << "In ポストインクリメント\n"; x++; y++; return( *this ); } int main( void ) { point o1(3,5); point o2(12,6); int x, y; o1++; ++o2; o1.get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; o2.get( x, y ); cout << "x = " << x << ", y = " << y << "\n"; }
結果は下記のようになります。
In ポストインクリメント In プリインクリメント x = 4, y = 6 x = 13, y = 7
プリインクリメントとポストインクリメントが区別されて実行されていることが確認できました。
他の単項演算子も同様に処理できます。
(30)フレンド演算子関数の使い方
メンバ演算子関数には黙示的にthisポインタを通じて左側のオペランドが引き渡されていましたが、
フレンド関数はthisポインタを持たないため明示的にすべてのパラメータを引き渡さなければなりません。
とりあえずフレンド演算子関数を使ってオーバーロードしてみましょう。
(ファイル名:30.opfriend1.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } friend point operator+( point, point ); }; point operator+( point obj1, point obj2 ) { point temp; temp.x = obj1.x + obj2.x; temp.y = obj1.y + obj2.y; return( temp ); } int main( void ) { point o1(20,30); point o2(5,11); int x, y; (o1+o2).get(x,y); cout << "x = " << x << ", y = " << y << "\n"; }
operator+演算子関数の引数が2つになっていることに注意してください。
他は特に変わったところはありません。
実行結果は下記のようになります。
x = 25, y = 41
o1とo2のメンバそれぞれの値を加算した結果が表示されました。
メンバ演算子関数の場合、右側に組み込み型を使うことはできるのですが、左側に組み込み型を使うことはできませんでした。
obj + 10; // ← この演算はできる
10 + obj; // ← この演算ができない!!
ですがフレンド演算子関数を用いますと、引数を2つ必要とするためこの演算が可能になります。
では組み込み型用のサンプルプログラムを示します。
(ファイル名:30.opfriend2.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } friend point operator+( point, int ); friend point operator+( int, point ); }; point operator+( point obj, int num ) { point temp; temp.x = obj.x + num; temp.y = obj.y + num; return( temp ); } point operator+( int num, point obj ) { point temp; temp.x = num + obj.x; temp.y = num + obj.y; return( temp ); } int main( void ) { point o1(20,30); point o2(5,11); int x, y; (o1+9).get(x,y); cout << "x = " << x << ", y = " << y << "\n"; (13+o2).get(x,y); cout << "x = " << x << ", y = " << y << "\n"; }
実行結果は下記のようになります。
x = 29, y = 39 x = 18, y = 24
フレンド演算子関数を使って ++ や -- をオーバーロードする場合には「参照」を使う必要があります。
なぜならオペランドに変更が加えられるからです。
ちなみにポインタ渡しをしてみたらエラーになりました。あくまで「参照」である必要があります。
ではサンプルプログラムを示します。
(ファイル名:30.opfriend3.cpp)
#include <iostream> using namespace std; class point { int x, y; public: point( int i = 0, int j = 0 ) { x = i; y = j; } void get( int &i, int &j ) { i = x; j = y; } friend point operator++( point & ); friend point operator++( point &, int ); }; point operator++( point &obj ) { cout << "In プリインクリメント\n"; ++obj.x; ++obj.y; return( obj ); } point operator++( point &obj, int num ) { cout << "In ポストインクリメント\n"; obj.x++; obj.y++; return( obj ); } int main( void ) { point o1(14,72); point o2(99,65); int x, y; ++o1; o1.get(x,y); cout << "x = " << x << ", y = " << y << "\n"; o2++; o2.get(x,y); cout << "x = " << x << ", y = " << y << "\n"; }
実行結果は下記のようになります。
In プリインクリメント x = 15, y = 73 In ポストインクリメント x = 100, y = 66
プリインクリメント、ポストインクリメント、それぞれ実行されているのが確認できました。
(31)代入演算子のオーバーロード
代入 = が実行されるとデフォルトではバイナリレベルでコピーされます。
このとき動的にメモリが割り当てられていると問題が発生します(使い方にもよります)。
動的にメモリが割り当てられている場合には =
をオーバーロードしてメモリを確保する必要があります(しつこいですが使い方にもよります)。
そんなわけでサンプルプログラムを示します。
(ファイル名:31.dainyu.cpp)
#include <iostream> #include <string.h> using namespace std; class strtype { char *p; int len; public: strtype( char * ); ~strtype(); char *get( void ) { return p; } strtype &operator=( strtype & ); }; strtype::strtype( char *pstr ) { int l; l = strlen( pstr ) + 1; p = new char [l]; if( ! p ) { cout << "メモリ確保エラー\n"; exit(1); } len = l; strcpy( p, pstr ); } strtype::~strtype() { delete [] p; } strtype & strtype::operator=( strtype &obj ) { cout << "In operator=\n"; // 更にメモリが必要かをチェック if( len < obj.len ) { delete [] p; p = new char [obj.len]; if( ! p ) { cout << "メモリ確保エラー\n"; exit(1); } len = obj.len; } strcpy( p, obj.p ); return( *this ); } int main( void ) { strtype s1((char *)"Hello"); strtype s2((char *)"World!!"); strtype s3((char *)""); s3 = s1; s1 = s2; cout << s3.get() << "\n"; cout << s1.get() << "\n"; }
operator=関数では現状のメモリサイズをチェックし、
不十分であれば現在確保されているメモリを解放し、再確保してから文字列をコピーしています。
実行結果は下記のようになります。
In operator= In operator= Hello World!!
(32)配列要素演算子のオーバーロード
配列要素演算子 [] をオーバーロードするときは2項演算子とみなします。
戻り値の型 operator[]( int index ) { // 処理 }
indexの型がintになっていますが、他の型でも構いません。ただし通常は整数型です。
obj[3]とプログラムで記述した場合、
operator[](3)
という関数呼び出しに置き換えられます。
※重要
上の説明中のobj[3]というのはobjオブジェクトの3番目ではないので注意!。
obj[3]は何を表しているのかというと「objオブジェクトが扱っている何らかのデータの3番目」です。
ではサンプルプログラムを示します。
(ファイル名:32.oparr.cpp)
#include <iostream> using namespace std; const int SIZE = 3; class atype { int a[SIZE]; public: atype( void ) { for( int i = 0; i < SIZE; i++ ) a[i] = i; } int &operator[]( int j ) { return a[j]; } }; int main() { atype obj; int i; for( i = 0; i < SIZE; i++ ) cout << obj[i] << " "; cout << "\n"; obj[1] = 19; for( i = 0; i < SIZE; i++ ) cout << obj[i] << " "; cout << "\n"; return( 0 ); }
operator[]関数の戻り値を「参照」にしているため、
代入式の左辺に記述すれば要素に代入できます。
上記プログラムの実行結果は下記のようになります。
0 1 2 0 19 2
使い方によっては安全な配列を作れます。
安全な配列とは、要素の数より大きな値を指定して範囲外のメモリにアクセスすることを防ぐ配列のことです。
(33)親クラスのアクセス制御
class クラス名 : アクセス指定子 親クラス名 [ // 定義など };
アクセス指定子はprivate、public、protectedの3種類あります。
まずprivateを指定すると親クラスのpublicメンバは子クラスのprivateメンバになります。
次にpublicを指定すると親クラスのpublicメンバは子クラスのpublicメンバになります。
protectedに関しては後ほど説明します。
では最初にアクセス指定子としてpublicを使ったサンプルプログラムを示します。
(ファイル名:33.paraccess1.cpp)
#include <iostream> using namespace std; class parent { int x; public: void setx( int num ) { x = num; } void showx( void ) { cout << x << "\n"; } }; class child : public parent { int y; public: void sety( int num ) { y = num; } void showy( void ) { cout << y << "\n"; } }; int main( void ) { child obj; obj.setx( 5 ); obj.sety( 10 ); obj.showx(); obj.showy(); }
エラー無くコンパイルできます。
続いてアクセス指定子にprivateを使ったサンプルプログラムを示します。
上のプログラムのアクセス指定子をpublicからprivateに変えただけです。
(ファイル名:33.paraccess2.cpp)
#include <iostream> using namespace std; class parent { int x; public: void setx( int num ) { x = num; } void showx( void ) { cout << x << "\n"; } }; class child : private parent { // privateを指定しているのでエラーが発生します。 int y; public: void sety( int num ) { y = num; } void showy( void ) { cout << y << "\n"; } }; int main( void ) { child obj; obj.setx( 5 ); // エラー発生 obj.sety( 10 ); obj.showx(); // エラー発生 obj.showy(); }
これをコンパイルすると下記のようなエラーになります。
親クラスのpublicメンバがprivateメンバになってしまったため発生するエラーです。
source\32.paraccess2.cpp: In function 'int main()': source\32.paraccess2.cpp:22:14: error: 'void parent::setx(int)' is inaccessible within this context 22 | obj.setx( 5 ); | ^ source\32.paraccess2.cpp:7:7: note: declared here 7 | void setx( int num ) { x = num; } | ^~~~ source\32.paraccess2.cpp:22:14: error: 'parent' is not an accessible base of 'child' 22 | obj.setx( 5 ); | ^ source\32.paraccess2.cpp:25:12: error: 'void parent::showx()' is inaccessible within this context 25 | obj.showx(); | ^ source\32.paraccess2.cpp:8:7: note: declared here 8 | void showx( void ) { cout << x << "\n"; } | ^~~~~ source\32.paraccess2.cpp:25:12: error: 'parent' is not an accessible base of 'child' 25 | obj.showx(); | ^
(34)protectedアクセス指定子
protected属性のメンバは、宣言されたクラス(親クラス)ではprivateとして扱われ、子クラスからはアクセス可能です。
孫クラスや外部関数からはアクセスできません。
表にすると下記のようになります。
アクセス指定子 | 親クラス | 子クラス | 孫クラス・外部関数 |
---|---|---|---|
private | 見える | 見えない | 見えない |
protected | 見える | 見える | 見えない |
public | 見える | 見える | 見える |
また、クラス継承時のアクセス指定子は下記のような関係になります。
- publicでクラスを継承するとprotectedメンバはprotected扱いのままです。
- privateでクラスを継承するとprotectedメンバはprivate扱いになります。
- protectedで継承するとpublicとprotectedメンバはprotected扱いになります。
(35)コンストラクタとデストラクタと継承
クラスを継承したときのコンストラクタとデストラクタの実行タイミングは、
コンストラクタの場合 ⇒ 親クラスが実行された後、子クラスが実行されます。
デストラクタの場合 ⇒ 子クラスが実行された後、親クラスが実行されます。
理由は、コンストラクタの場合は親クラスのコンストラクタが実行されてからでないと準備が整っていないので子クラスのコンストラクタを実行できません。
デストラクタの場合は逆で、親クラスを先に破棄してしまうと、中途半端に破棄される形になって子クラスのデストラクタを実行できなくなります。
ではサンプルプログラムでそうなっているか確認してみましょう。
(ファイル名:35.condesinher.cpp)
#include <iostream> using namespace std; class parent { public: parent() { cout << "In 親コンストラクタ\n"; } ~parent() { cout << "In 親デストラクタ\n"; } }; class child : public parent { // privateを指定しているのでエラーが発生します。 public: child() { cout << "In 子コンストラクタ\n"; } ~child() { cout << "In 子デストラクタ\n"; } }; int main( void ) { child obj; }
実行結果は下記のようになります。
In 親コンストラクタ In 子コンストラクタ In 子デストラクタ In 親デストラクタ
(36)親クラスのコンストラクタへ引数を渡す方法
継承した親クラスのコンストラクタへ引数を渡すには、
まず子クラスに必要なすべての引数を渡して、
子クラスから親クラスへ引数を渡す形になります。
もう少し具体的に書きますと、
- 子クラスに必要な引数をすべて渡します。
- 子クラスのコンストラクタ宣言の拡張形式を使って適切な引数を親クラスへ渡します。
子クラスから親クラスへと引数を渡す構文は次のようになります。
ひとつ注意して頂きたいことがあります。
それは下記の構文は「子クラスのコンストラクタ本体を定義するところ」の構文です。
宣言するだけのところでは記述できませんのでご注意ください。
子クラス ( 引数リスト ) : 親クラス ( 引数リスト ) { // 子クラス コンストラクタ本体 }
下記にサンプルプログラムを示します。
(ファイル名:36.parcon.cpp)
#include <iostream> using namespace std; class parent { public: parent( int x, int y ) { cout << "In 親コンストラクタ 引数 = "; cout << x << " " << y << "\n"; } ~parent() { cout << "In 親デストラクタ\n"; } }; class child : public parent { // privateを指定しているのでエラーが発生します。 public: child( int x, int y, int z ); ~child(); }; child::child( int x, int y, int z ): parent( x, y ) { cout << "In 子コンストラクタ 引数 = "; cout << x << " " << y << " " << z << "\n"; } child::~child() { cout << "In 子デストラクタ\n"; } int main( void ) { child obj(10,20,30); }
実行結果は下記のようになります。
In 親コンストラクタ 引数 = 10 20 In 子コンストラクタ 引数 = 10 20 30 In 子デストラクタ In 親デストラクタ
引数が親クラスのコンストラクタに正しく渡っていることが確認できました。
(37)多重継承
多重継承には2種類あります。
①クラスの階層を積み重ねる方法。
②複数のクラスを直接継承する方法。
①は今までのところを理解していれば分かると思います。
では②の方法について解説します。
複数のクラスを継承するには、クラスの定義のところで
class 子クラス名 : アクセス指定子 親クラス名1 , アクセス指定子 親クラス名2 , .... アクセス指定子 親クラス名N { // クラス定義 };
のように記述します。
コンストラクタに引数を渡す場合はコンストラクタ定義のところで、
子クラス ( 引数リスト ) : 親クラス1 ( 引数リスト ), 親クラス2 ( 引数リスト ), .... 親クラスN ( 引数リスト ) { // 子クラス コンストラクタ本体 }
のように定義します。
※コンストラクタは定義されている左側から右側へ順番に実行されます。
デストラクタは逆に右側から左側へ順番に実行されます。
下記にサンプルプログラムを示します。
(ファイル名:37.mulinher.cpp)
#include <iostream> using namespace std; class p1 { public: p1( int n ) { cout << "In p1コンストラクタ 引数 = "; cout << n << "\n"; } ~p1() { cout << "In p1デストラクタ\n"; } }; class p2 { public: p2( int n ) { cout << "In p2コンストラクタ 引数 = "; cout << n << "\n"; } ~p2() { cout << "In p2デストラクタ\n"; } }; class child : public p1, public p2 { public: child( int x, int y, int z ); ~child(); }; child::child( int x, int y, int z ): p1( x ), p2( z ) { cout << "In 子コンストラクタ 引数 = "; cout << x << " " << y << " " << z << "\n"; } child::~child() { cout << "In 子デストラクタ\n"; } int main( void ) { child obj(50,60,70); }
実行結果は下記のようになります。
In p1コンストラクタ 引数 = 50 In p2コンストラクタ 引数 = 70 In 子コンストラクタ 引数 = 50 60 70 In 子デストラクタ In p2デストラクタ In p1デストラクタ
p1、p2のコンストラクタ、デストラクタの呼び出される順番をご確認ください。
それとコンストラクタに渡っている引数もご確認ください。
(38)仮想基本クラス
下記のようにクラスを継承したとします。
親クラス(base) ← 子クラス(child1) ←┐ ├ 孫クラス(grand) 親クラス(base) ← 子クラス(child2) ←┘
(矢印は継承元を表します。一般的なC++の表現です。)
このとき孫クラス(grand)から親クラス(base)をみると、実体が2つあることになってしまいます。
どちらを参照したらよいか分からなくて困ってしまいますが(曖昧であると表現します)、
このとき孫クラスには親クラス(base)のコピーが1つしか含まれないようにする仕組みをC++は持っています。
この機能を仮想基本クラス(virtual base class)と言います。
子クラスが親クラス(base)を継承するときvirtualとして継承することで、
子クラスオブジェクト内に2つのコピーが存在しないようにすることができます。
子クラスが仮想基本クラスを継承するようにするには、親クラスのアクセス指定子の前にvirtualを付けます。
下記にサンプルプログラムを示します。
(ファイル名:38.virtualbase.cpp)
#include <iostream> using namespace std; class base { public: int i; }; class child1 : virtual public base { // 仮想基本クラス(base)を継承 public: int j; }; class child2 : virtual public base { // 仮想基本クラス(base)を継承 public: int k; }; class grand : public child1, public child2 { public: int all_add(void){ return( i+j+k ); } }; int main( void ) { grand obj; obj.i = 3; // コピー1つなので曖昧さなし! obj.j = 5; obj.k = 7; cout << obj.all_add() << "\n"; }
仮想基本クラスを継承しているため曖昧さが無くなり、コンパイルエラーになりません。
試しにvirtualをとってコンパイルしてみてください。エラーになります。
ちなみに上のプログラムを実行した結果は下記のようになります。
15
(39)子クラスへのポインタ
親クラスで宣言されたポインタで子クラスのポインタを代入できます。
要は親クラスのデータの後に子クラスで追加されたデータが付くということだと思われます。
例
parent *p; parent objp; child objc; p = objp; // これは問題なし p = objc; // これもコンパイルエラーにならない! }
p++としたとき、親クラス分しかプラスされないので子クラスへのポインタを入れて置いた場合は、
次の子クラスを指すわけではないので注意してください。
(40)仮想関数
仮想関数とは親クラスでどのように実装したらよいか決まらないときにvirtual定義しておき、
子クラスでオーバーライドして正規の動作をする関数を実装するための仮関数です。
仮とはいえ子クラスでオーバーライドしない場合は親クラスの仮想関数が実行されるので、
それなりの動作をするものでなければなりません。
下記にサンプルプログラムを示します。
(ファイル名:40.virtualfunc.cpp)
#include <iostream> using namespace std; class parent { public: int i; parent( int num ) { i = num; } virtual void calc( void ) { // 仮想関数calc cout << "In (親)仮想関数 "; cout << i << "\n"; } }; class child1 : public parent { // 親クラス(parent)を継承 public: child1( int num ) : parent( num ) {} // 何もしない(引数を渡すだけの)コンストラクタ void calc( void ) { // 仮想関数オーバーライド cout << "In 子1 calc関数 "; cout << i+i << "\n"; } }; class child2 : public parent { // 親クラス(parent)を継承 public: child2( int num ) : parent( num ) {} // 何もしない(引数を渡すだけの)コンストラクタ void calc( void ) { // 仮想関数オーバーライド cout << "In 子2 calc関数 "; cout << i+i+i << "\n"; } }; class child3 : public parent { // 親クラス(parent)を継承 public: child3( int num ) : parent( num ) {} // 何もしない(引数を渡すだけの)コンストラクタ // 仮想関数をオーバーライドしない!! }; int main( void ) { parent *p; child1 obj1(10); child2 obj2(10); child3 obj3(10); p = &obj1; p->calc(); p = &obj2; p->calc(); p = &obj3; p->calc(); }
実行結果は下記のようになります。
In 子1 calc関数 20 In 子2 calc関数 30 In (親)仮想関数 10
関数のオーバーロードのときと異なり、
引数や戻り値が同じでも呼び出すポインタが異なっていれば適切な関数が呼び出されることが確認できました。
最後に、仮想関数は継承されるとvirtual特性も継承されるので憶えておいてください。
(41)純粋仮想関数と抽象クラス
オーバーライドが確実に行われるようにするには、
下記のような一般形式を用います。
virtual 型 関数名 ( 引数リスト ) = 0;
0と等しくなるように設定することで、(親クラスでは)この関数の本体を定義しないことをコンパイラに伝えます。
子クラスでは純粋仮想関数をオーバーライドしないとコンパイルエラーになるため、再定義を確実に行うことができます。
純粋仮想関数を1つ以上含むクラスは抽象クラスと呼ばれます。
⇒ 抽象クラスは不完全な形なのでオブジェクトを作成できません。
⇒ 抽象クラスは継承されるためにあるクラスですが、抽象クラスへのポインタは作成可能です。
(ファイル名:41.purevirtual.cpp)
#include <iostream> using namespace std; class parent { public: int i; parent( int num ) { i = num; } virtual void calc( void ) = 0; // 純粋仮想関数calc }; class child1 : public parent { // 親クラス(parent)を継承 public: child1( int num ) : parent( num ) {} // 何もしない(引数を渡すだけの)コンストラクタ void calc( void ) { // 仮想関数オーバーライド cout << "In 子1 calc関数 "; cout << i+i << "\n"; } }; class child2 : public parent { // 親クラス(parent)を継承 public: child2( int num ) : parent( num ) {} // 何もしない(引数を渡すだけの)コンストラクタ // 純粋仮想関数をオーバーライドしないとコンパイルエラーになります。 // 下記のcalc関数のコメント記号を外してオーバーライドするとコンパイルが通って実行ファイルが出来上がります。 // void calc( void ) { // 仮想関数オーバーライド // cout << "In 子2 calc関数 "; // cout << i+i+i << "\n"; // } }; int main( void ) { parent *p; child1 obj1(10); child2 obj2(10); p = &obj1; p->calc(); p = &obj2; p->calc(); }
上記のプログラムをコンパイルすると下記のようなエラーになります。
child2クラスのcalc関数のところのコメント記号を外してコンパイルするとコンパイルが通って実行ファイルが出来上がります。
source\40.purevirtual.cpp: In function 'int main()': source\40.purevirtual.cpp:35:9: error: cannot declare variable 'obj2' to be of abstract type 'child2' 35 | child2 obj2(10); | ^~~~ source\40.purevirtual.cpp:20:7: note: because the following virtual functions are pure within 'child2': 20 | class child2 : public parent { // 親クラス(parent)を継承 | ^~~~~~ source\40.purevirtual.cpp:8:15: note: 'virtual void parent::calc()' 8 | virtual void calc( void ) = 0; // 純粋仮想関数calc | ^~~~
calc関数のコメントを外してオーバーライドしたときの実行結果は下記のようになります。
In 子1 calc関数 20 In 子2 calc関数 30
(42)汎用関数
「データ型は異なるが、アルゴリズムは同じ」という処理があった場合、
それを汎用関数として定義し、C++が自動的に型に合わせた関数をオーバーロードして実行するということが可能です。
汎用関数(テンプレート関数)は下記のような一般形式で定義されます。
template < class 型 > 戻り値の型 関数名 ( 引数リスト ) { // 関数本体 }
2つの変数の値を入れ換えるサンプルプログラムを示します。
(ファイル名:42.genefunc1.cpp)
#include <iostream> using namespace std; template <class X> void org_swap( X &a, X &b ) { X temp; temp = a; a = b; b = temp; } int main( void ) { int a = 50, b = 100; double x = 50.5, y = 100.5; cout << "初期値 a = " << a << ", b = " << b << "\n"; cout << "初期値 x = " << x << ", y = " << y << "\n"; org_swap( a, b ); org_swap( x, y ); cout << "入れ換え後 a = " << a << ", b = " << b << "\n"; cout << "入れ換え後 x = " << x << ", y = " << y << "\n"; }
実行結果は下記のようになります。
初期値 a = 50, b = 100 初期値 x = 50.5, y = 100.5 入れ換え後 a = 100, b = 50 入れ換え後 x = 100.5, y = 50.5
汎用関数を1つ作っただけで2つの型の入れ換え処理ができてしまいました。
template文ではカンマで区切ったリストを使って複数の汎用データ型を定義することができます。
2つの汎用データ型を使ったサンプルプログラムを示します。
(ファイル名:42.genefunc2.cpp)
#include <iostream> using namespace std; template <class type1, class type2> void myfunc( type1 a, type2 b, int c ) { cout << a << " " << b << " " << c << "\n"; } int main( void ) { myfunc( 20.5, "ok!", 3 ); myfunc( 120, 0.5, 10 ); }
実行結果は下記のようになります。
20.5 ok! 3 120 0.5 10
テンプレート関数は明示的にオーバーロードすることが可能です。
(43)汎用クラス
汎用関数だけでなく、汎用クラスも作成できます。
汎用クラス定義の一般形式は下記のようになります。
template < class 型 > class クラス名 { // クラス本体 };
汎用クラスのメンバ関数は自動的に汎用関数になります。
そのためtemplateを使って明示的に指定する必要はありません。
汎用クラスを作成したら下記の一般形式を使ってオブジェクトを作成します。
クラス名 < 型 > obj;
下記にサンプルプログラムを示します。
(ファイル名:43.geneclass.cpp)
#include <iostream> using namespace std; template <class TYPE> class myclass { TYPE data; public: myclass( TYPE d ) { data = d; } void show( void ) { cout << data << "\n"; } }; int main( void ) { myclass <double> obj1(20.5); myclass <char *> obj2((char *)"Hello World!!"); myclass <int> obj3(100); obj1.show(); obj2.show(); obj3.show(); }
とりあえず最低限のサンプルプログラムになってしまいましたが、使い方はあなた次第です。
実行結果は下記のようになります。
20.5 Hello World!! 100
(44)例外
「例外」は実行時のエラー処理を簡潔に記述することができます。
try { // 例外が発生する可能性があるコードを記述 } catch( type1 arg ) { // 例外処理1 } catch( type2 arg ) { // 例外処理2 } ... catch( typeN arg ) { // 例外処理N } catch( ... ) { // その他すべての例外処理 }
という感じで記述します。
「type」はthrowしたデータの型を指定し、argはそれを受け取る変数名を記述します。
「例外処理」のところは、例外が発生したときの後処理(メッセージを表示するなど)を記述します。
catchのところに catch(...) と記述するとすべての例外を受け取ります。
関数定義のところで下記のように記述すると、「型リスト」に指定した例外だけ投入(受け取る)ことができます。
それ以外の例外が発生した場合は実行時エラーとなります。
戻り値の型 関数名 ( 引数リスト ) throw (型リスト ) { // 関数本体 }
catchブロックでは更にthrowすることができます。
そのとき単に throw; と記述するとcatchした型が使用されます。
下記にサンプルプログラムを示します。
(ファイル名:44.exception.cpp)
#include <iostream> using namespace std; int main( void ) { try { throw 10; // throw (char *)"error!!"; // throw 32.5; } catch( int e ) { cout << e << "\n"; } catch( char *e ) { cout << e << "\n"; } catch( ... ) { cout << "その他の例外\n"; } return( 0 ); }
tryブロックの有効なthrowを変えてみて、どの例外処理が実行されるかを確認してみてください。
ちなみに上記プログラムの実行結果は下記のようになります。
10
(45)スタティックなクラスメンバ
static宣言されたメンバ変数は、そのクラスのオブジェクトがいくら作られても、単にその1つの変数を共有するだけになります。
子クラスを作った場合も同様に1つの変数を共有します。
staticメンバ変数は宣言しただけでは定義されません(つまり実体がありません)。
実体を作る(定義する)にはスコープ解決演算子を使って下記のように定義します。
型 クラス名 :: スタティック変数名;
ではサンプルプログラムを示します。
(ファイル名:45.static.cpp)
#include <iostream> using namespace std; class myclass { public: static int i; void seti( int num ) { i = num; } int geti( void ) { return i; } }; int myclass::i; // スコープ解決演算子を使って指定しています。 int main( void ) { myclass obj1, obj2; myclass::i = 99; cout << obj1.geti() << " " << obj2.geti() << "\n"; obj1.seti( 10 ); cout << obj1.geti() << " " << obj2.geti() << "\n"; return( 0 ); }
実行結果は下記のようになります。
99 99 10 10
obj1とobj2の両方が共通のグローバル変数(myclass::i)を使っているのが確認できました。
(46)リンクとasm文
他の言語とリンクすることができます。
一般形式は下記のようになります。
extern "language" 関数のプロトタイプ;
複数の関数を指定する場合は下記のように記述します。
extern "language" { 関数のプロトタイプ1; 関数のプロトタイプ2; ..... 関数のプロトタイプN; }
例.C言語の関数としてリンクする
extern "C" int func( int x );
アセンブリ言語のコードを埋め込むには、
asm (op-code); asm op-code; asm op-code newline // 改行コードで終了します asm { opcode1 opcode2 ..... opcodeN }
newlineは改行コードを入れることを意味します。
※アセンブリ言語はコンパイラの実装に左右されます。
(47)変換関数
変換関数はオブジェクトを、オブジェクトが使われている式の型と互換性のある型に自動的に変換します。
変換関数の一般形式は次のようになります。
operator 変換後の型() { return 変換が実行された後の値; }
変換関数は変換を実行するクラスのメンバでなければなりません。
変換関数は継承され、virtualとすることもできます。
変換関数を使うと変換後の型を必要とする式の中に直接オブジェクトを含めることができます。
下記にサンプルプログラムを示します。
(ファイル名:47.transfunc.cpp)
#include <iostream> using namespace std; class myclass { int x,y; public: myclass( int i, int j ) { x = i; y = j; } operator int() { return x+y; } // 変換関数 }; int main( void ) { myclass obj1( 5, 6), obj2( 11, 12 ); int a; a = obj1; cout << a << "\n"; a = obj1 + obj2; cout << a << "\n"; return( 0 ); }
実行結果は下記のようになります。
11 34
変換関数でxとyの値を加算した数をリターンしているので上記のような結果になります。